/*
 * Copyright (c) 2011, Jordan Taekema
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.andactive;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A simple cache using java.util.concurrent.  This will cache most recently inserted entities.
 * <p/>
 * Created 7/30/11 2:44 AM by Jordan
 */
final class ActiveRecordCache<T extends ActiveRecord> {


	/**
	 * Used to store all of the different cache's for each object type.
	 */
	private static Map<String, ActiveRecordCache<? extends ActiveRecord>> caches = new ConcurrentHashMap<String, ActiveRecordCache<? extends ActiveRecord>>();

	/**
	 * Used to store the active record entities.
	 */
	private ConcurrentHashMap<Long, T> cache;

	/**
	 * Used to restrict the size of the cache map.
	 */
	private ConcurrentLinkedQueue<Long> queue;

	/**
	 * the max size that the cache is allowed to be
	 */
	private int maxSize;

	/**
	 * Using this integer because ConcurrentLinkedQueue.size is not constant time.
	 */
	private AtomicInteger size;

	/**
	 * @param maxSize the max number of entities to store in the cache
	 */
	ActiveRecordCache(int maxSize) {
		this.maxSize = maxSize;
		clear();
	}

	/**
	 * Puts an entity in the cache.
	 *
	 * @param val the entity to store in the cache
	 */
	void put(T val) {
		long key = val.id;

		if (key <= 0)
			throw new IllegalArgumentException("Primary key should not be 0 or negative.");

		// if it already exists just return
		if (cache.contains(key))
			return;

		size.incrementAndGet();

		while (size.get() > maxSize && maxSize > 0) {
			Long toRemove = queue.poll();

			if (toRemove == null)
				break;

			remove(toRemove); // note: this decrements size
		}

		cache.put(key, val);
		queue.add(key);
	}

	/**
	 * @param key the primary key to retrieve the entity from this cache
	 * @return the entity from the cache if it exists; null otherwise
	 */
	T get(Long key) {
		return cache.get(key);
	}

	/**
	 * @param key The primary key of the entity to remove from the cache
	 * @return the entity that was removed from the cache, null if it was not found
	 */
	T remove(Long key) {
		T entry = cache.remove(key);
		if (entry != null) {
			size.decrementAndGet();
			return entry;
		}
		return null;
	}

	/**
	 * clears the cache
	 */
	void clear() {
		size = new AtomicInteger();
		cache = new ConcurrentHashMap<Long, T>(maxSize);
		queue = new ConcurrentLinkedQueue<Long>();
	}

	/**
	 * @return the current size of the cache
	 */
	int size() {
		return size.get();
	}

	@SuppressWarnings("unchecked")
	static <T extends ActiveRecord> ActiveRecordCache<T> getCache(Class<T> type) {
		ActiveRecordCache<T> cache = (ActiveRecordCache<T>) caches.get(type.getName());
		if (cache == null) {
			cache = new ActiveRecordCache<T>(ActiveRecordConfig.MAX_ENTITY_CACHE_SIZE);
			caches.put(type.getName(), cache);
		}
		return cache;
	}

	static <T extends ActiveRecord> T get(Class<T> type, Long key) {
		return getCache(type).get(key);
	}

	static <T extends ActiveRecord> void put(Class<T> type, T entity) {
		getCache(type).put(entity);
	}

	static <T extends ActiveRecord> T remove(Class<T> type, Long key) {
		return getCache(type).remove(key);
	}

	/**
	 * clear the cache for a specific type, pass null to clear them all
	 *
	 * @param type
	 * @param <T>
	 */
	static <T extends ActiveRecord> void clear(Class<T> type) {
		if (type == null) {
			for (ActiveRecordCache cache : caches.values()) {
				cache.clear();
			}

			caches = new ConcurrentHashMap<String, ActiveRecordCache<? extends ActiveRecord>>();
		} else {
			ActiveRecordCache<T> cache = getCache(type);

			cache.queue.clear();
			cache.cache.clear();
			cache.size.set(0);
		}
	}
}
